---
title: 'React Query (v4)'
description: The React Query client provides a lightweight, type-safe way to make HTTP requests using your ts-rest contract.
---

## Installation

<InstallTabs packageName="@ts-rest/react-query @tanstack/react-query@4" />

This is a client using `@tanstack/react-query` under the hood.

### Initializing QueryClientProvider

After installation, ensure that your React application is wrapped with `react-query`'s `QueryClientProvider`. This provider is essential for managing the state and lifecycle of your queries and mutations.

```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
```

For more detailed information, refer to the [official `react-query` documentation](https://tanstack.com/query/v4/docs/react/reference/QueryClientProvider).

## initQueryClient

The below snippet is how you'd create a query client, this is pretty much the same structure as the `@ts-rest/core` client.

```tsx
import { initQueryClient } from '@ts-rest/react-query';

export const client = initQueryClient(router, {
  baseUrl: 'http://localhost:3333',
  baseHeaders: {},
  api?: () => ... // <- Optional Custom API Fetcher (see below)
});
```

To customise the API, follow the same docs as the core client [here](/docs/core/custom)

<Callout title="JSON Query Parameters">

By default, ts-rest encodes query parameters as regular URL encoded strings (with support for nested objects, arrays etc) with full decode compatibility from [`qs`](https://www.npmjs.com/package/qs)

To encode query parameters as JSON, you can use the `jsonQuery` option, please note you'll need to configure your backend to support decoding JSON query parameters.

```tsx
export const client = initQueryClient(router, {
  ...,
  jsonQuery: true
});

```

</Callout>

## useQuery and useMutation

Once you've created a client using `initQueryClient`, you may traverse the object (in the exact same structure as your contract layout) to find the query or mutation you want to use.

```tsx
const queryResult = client.posts.get.useQuery(
  ['posts'], // <- queryKey
  { params: { id: '1' } }, // <- Query params, Params, Body etc (all typed)
  { staleTime: 1000 }, // <- react-query options (optional)
);
```

<Callout title="Design Philosophy">

The design philosophy of `@ts-rest/react-query` is to make it as easy as possible to use `react-query` with `@ts-rest`. This means that the `useQuery` and `useMutation` hooks are as close to the original `react-query` hooks as possible, as such we don't abstract away from the query keys or the options. Only the query function itself!

</Callout>

```tsx
const App = () => {
  // Effectively a useQuery hook
  const { data, isLoading, error } = client.posts.get.useQuery(['posts']);

  // Effectively a useMutation hook
  const { mutate, isLoading } = client.posts.create.useMutation();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (data?.status !== 200) {
    return <div>Error</div>;
  }

  return (
    <div>
      {data.body.map((post) => (
        <p key={post.id}>post.title</p>
      ))}
    </div>
  );
};
```

<Callout type="info" title="Response Structure">

When destructing the response from `useQuery` or `useMutation`, remember that ts-rest returns a `status` and `body` property, so you'll need to destructure those as well.

The reason for this is error handling! Please see the [Relevant Docs](/docs/core/errors#client-error-typing)

</Callout>

## Error Handling

If a request fails, the `error` property will be set to the response from the server, or the thrown error by `fetch`. This is the same as the `data` property for successful requests.

By default, the type of the `error` property on the React Query hooks will only be set as `{ status: ...; body: ...; headers: ... }`, where status is a non-2xx status code, and `body`
set to your response schema for status codes defined in your contract, or `unknown` for status codes not in your contract.

However, queries and mutations can also fail for other reasons, such as network failures, or CORS errors.
In such cases, the current default behavior may cause runtime errors if you try to access the `error.(status|body|headers)` properties without first checking if the error is an instance of `Error`.

To include the `Error` exception type, you can set the `includeThrownErrorsInErrorType` option to `true` when initializing the client.

```tsx
export const client = initQueryClient(router, {
  ...,
  includeThrownErrorsInErrorType: true
});
```

This will force you the handle these errors if you attempt to access the `error.status` property, without first checking if the error is an instance of `Error`.

This will be enabled by default starting from `@ts-rest/react-query` v4.0.0.

## Regular Query and Mutations

`@ts-rest/react-query` allows for a regular fetch or mutation if you want, without having to initialise two different clients, one with `@ts-rest/core` and one with `@ts-rest/react-query`.

```typescript
// Normal fetch
const { body, status } = await client.posts.get.query();

// useQuery hook
const { data, isLoading } = client.posts.get.useQuery();
```

## useInfiniteQuery

One fantastic feature of `react-query` is the ability to create infinite queries. This is a great way to handle pagination.

[Prisma's Docs](https://www.prisma.io/docs/concepts/components/prisma-client/pagination) explain the concepts of cursor and offset pagination fantastically, especially if you use Prisma client with `@ts-rest`

### Cursor Pagination

This is a simple cursor based pagination example,

```typescript
const { isLoading, data, hasNextPage, fetchNextPage } = useInfiniteQuery(
  queryKey,
  ({ pageParam = 1 }) => pageParam,
  {
    getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
  },
);
```

### Offset Pagination

This example specifically uses an API with `skip` and `take` query parameters, so this is requires slightly more configuration than a regular query (hence the complicated looking getNextPageParam)

```tsx
const PAGE_SIZE = 5;

export function Index() {
  const { isLoading, data, hasNextPage, fetchNextPage } =
    client.getPosts.useInfiniteQuery(
      ['posts'],
      ({ pageParam = { skip: 0, take: PAGE_SIZE } }) => ({
        query: { skip: pageParam.skip, take: pageParam.take },
      }),
      {
        getNextPageParam: (lastPage, allPages) =>
          lastPage.status === 200
            ? lastPage.body.count > allPages.length * PAGE_SIZE
              ? { take: PAGE_SIZE, skip: allPages.length * PAGE_SIZE }
              : undefined
            : undefined,
      },
    );

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!data) {
    return <div>No posts found</div>;
  }

  const posts = data.pages.flatMap((page) =>
    page.status === 200 ? page.body.posts : [],
  );

  //...
}
```

## useQueries

`useQueries` ([useQueries docs](https://tanstack.com/query/v4/docs/react/reference/useQueries)) is a great way to fetch multiple queries at once, or to dynamically fetch queries based on some condition. This is great because normally the number of useQuery hooks per component is fixed.

```tsx
const queries = client.posts.get.useQueries({
  queries: [
    // <- This queries array can be changed at runtime!
    {
      queryKey: ['posts', '1'],
      params: {
        id: '1',
      },
    },
    {
      queryKey: ['posts', '2'],
      params: {
        id: '2',
      },
    },
  ],
});

if (queries.some((query) => query.isLoading)) {
  return <div>Loading...</div>;
}

return (
  <div>
    {queries.map((query) => (
      <p key={query.data?.body.id}>{query.data?.body.title}</p>
    ))}
  </div>
);
```

## QueryClient Helpers

In addition to the hooks provided, `@ts-rest/react-query` also provides a custom hook `useTsRestQueryClient` with wrapper functions around some `QueryClient` functions to help invoke the API as well as provide some typing.

```tsx
import { initQueryClient, useTsRestQueryClient } from '@ts-rest/react-query';

export const client = initQueryClient(router);

const App = () => {
  const apiQueryClient = useTsRestQueryClient(client);

  // You can either use apiQueryClient or client to call useQuery, useMutation, etc.
  const { data, isLoading, error } = apiQueryClient.posts.get.useQuery([
    'posts',
  ]);
  const { mutate, isLoading } = client.posts.create.useMutation();

  const createPost = async () => {
    return mutate(
      { body: { title: 'Hello World' } },
      {
        onSuccess: async (data) => {
          //  this is typed ^
          apiQueryClient.posts.get.setQueryData(['posts'], (oldPosts) => {
            //                               this is also typed ^
            return {
              ...oldPosts,
              body: [...oldPosts.body, data.body],
            };
          });
        },
      },
    );
  };

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (data?.status !== 200) {
    return <div>Error</div>;
  }

  return (
    <div>
      <button onClick={createPost}>Create Post</button>
      {data.body.map((post) => (
        <p key={post.id}>post.title</p>
      ))}
    </div>
  );
};
```

#### Functions

Following the same design philosophy as the hooks, these helpers follow the original `QueryClient` function APIs as closely as possible.
Essentially, the API is exactly the same, but instead of passing a `queryFn`, you pass the request parameters directly.

- `async fetchQuery(queryKey, args, options) => Promise<SuccessResponse>`
- `async fetchInfiniteQuery(queryKey, argsMapper, options) => Promise<InfiniteData<SuccessResponse>>`
- `async prefetchQuery(queryKey, args, options) => Promise<void>`
- `async prefetchInfiniteQuery(queryKey, argsMapper, options) => Promise<void>`
- `getQueryData(queryKey, filters) => SuccessResponse | undefined`
- `async ensureQueryData(queryKey, args, options) => Promise<SuccessResponse>`
- `getQueriesData(filters) => [QueryKey, SuccessResponse | undefined][]`
- `setQueryData(queryKey, updater) => SuccessResponse | undefined`

If you would like to use any of these functions outside a component or a hook, these functions are also available through the client directly, but you have to pass in the `QueryClient` instance as the first argument.

Functions such as `invalidateQueries` that neither exchange typed data nor invoke the API have no benefit of being wrapped. Therefore, call these functions directly through your `QueryClient` instance.

<Callout type="info" title="Error Handling">

Functions that may throw on failure such as `fetchQuery` will throw an error if the request fails or returns a non-2xx response.
Be sure to handle these errors appropriately.

</Callout>

## Troubleshooting

### `No QueryClient set, use QueryClientProvider to set one`

If you see this error despite having set a `QueryClient` using `QueryClientProvider`. Then you might have different versions of `@tanstack/react-query` installed in your project.

This can also happen in rare cases when ESM and CJS versions of the package are mixed by a bundler like Webpack.

If you have made sure that you are using the same version of `@tanstack/react-query` across your project, and are still having problems, you can work around this
by importing `@tanstack/react-query` from `@ts-rest/react-query/tanstack` instead of `@tanstack/react-query`. This will ensure that you are using the same version as the one used
by `@ts-rest/react-query`.

```tsx
import {
  QueryClient,
  QueryClientProvider,
} from '@ts-rest/react-query/tanstack';

const queryClient = new QueryClient();

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
```
